 /*******************************************************************************
  * Copyright (c) 2002, 2007 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
  * http://www.eclipse.org/legal/epl-v10.html
  *
  * Contributors:
  * IBM Corporation - initial API and implementation
  *******************************************************************************/
 package org.eclipse.ui.internal.cheatsheets.data;

 import java.io.IOException ;
 import java.io.InputStream ;
 import java.io.StringReader ;
 import java.net.URL ;
 import java.util.ArrayList ;

 import javax.xml.parsers.DocumentBuilder ;

 import org.eclipse.core.runtime.Assert;
 import org.eclipse.core.runtime.IStatus;
 import org.eclipse.core.runtime.Platform;
 import org.eclipse.core.runtime.Status;
 import org.eclipse.help.internal.UAElement;
 import org.eclipse.help.internal.UAElementFactory;
 import org.eclipse.help.internal.dynamic.DocumentProcessor;
 import org.eclipse.help.internal.dynamic.DocumentReader;
 import org.eclipse.help.internal.dynamic.ExtensionHandler;
 import org.eclipse.help.internal.dynamic.FilterHandler;
 import org.eclipse.help.internal.dynamic.IncludeHandler;
 import org.eclipse.help.internal.dynamic.ProcessorHandler;
 import org.eclipse.osgi.util.NLS;
 import org.eclipse.ui.cheatsheets.AbstractItemExtensionElement;
 import org.eclipse.ui.internal.cheatsheets.CheatSheetEvaluationContext;
 import org.eclipse.ui.internal.cheatsheets.CheatSheetPlugin;
 import org.eclipse.ui.internal.cheatsheets.ICheatSheetResource;
 import org.eclipse.ui.internal.cheatsheets.Messages;
 import org.eclipse.ui.internal.cheatsheets.composite.model.CompositeCheatSheetModel;
 import org.eclipse.ui.internal.cheatsheets.composite.parser.CompositeCheatSheetParser;
 import org.eclipse.ui.internal.cheatsheets.composite.parser.ICompositeCheatsheetTags;
 import org.eclipse.ui.internal.cheatsheets.composite.parser.IStatusContainer;
 import org.eclipse.ui.internal.cheatsheets.registry.CheatSheetItemExtensionElement;
 import org.eclipse.ui.internal.cheatsheets.registry.CheatSheetRegistryReader;
 import org.w3c.dom.Document ;
 import org.w3c.dom.NamedNodeMap ;
 import org.w3c.dom.Node ;
 import org.w3c.dom.NodeList ;
 import org.xml.sax.InputSource ;
 import org.xml.sax.SAXException ;
 import org.xml.sax.SAXParseException ;

 /**
  * Parser for the cheatsheet content files.
  *
  * Construct an intance of the CheatSheetDomParser.
  * Call <code>parse()</code>.
  * Then get the content items by calling
  * <code>getIntroItem()</code> and <code>getItemsList()</code>.
  * The title of the cheatsheet can be retrieved by calling
  * <code>getTitle()</code>.
  *
  */
 public class CheatSheetParser implements IStatusContainer {

     private static final String TRUE_STRING = "true"; //$NON-NLS-1$

     private DocumentBuilder documentBuilder;
     private DocumentProcessor processor;
     private ArrayList itemExtensionContainerList;
     
     // Cheatsheet kinds that can be parsed
 public static final int COMPOSITE_ONLY = 1;
     public static final int SIMPLE_ONLY = 2;
     public static final int ANY = 3;
     
     private IStatus status;

     private int commandCount;

     private int actionCount;
     

     /**
      * Java constructor comment.
      */
     public CheatSheetParser() {
         super();
         documentBuilder = CheatSheetPlugin.getPlugin().getDocumentBuilder();
     }
     
     /**
      * Gets the status of the last call to parse()
      */
     public IStatus getStatus() {
         return status;
     }
     
     public void addStatus(int severity, String message, Throwable exception) {
         status = ParserStatusUtility.addStatus(status, severity, message, exception);
     }

     /**
      * Converts any characters required to escaped by an XML parser to
      * their escaped counterpart.
      *
      * Characters XML escaped counterpart
      * < -> &lt;
      * > -> &gt;
      * & -> &amp;
      * ' -> &apos;
      * " -> &quot;
      *
      * Tags that will be ignored <b>, </b> and <br/>.
      *
      * @param text the string buffer to have its characters escaped
      * @return string buffer with any of the characters requiring XML escaping escaped
      */
     private StringBuffer escapeXMLCharacters(StringBuffer text) {
         // Set the maximum length of the tags to ignore
 final int MAXIMUM_TAG_LENGTH = 5;
         
         // Keep a local variable for the orignal string's length
 int length = text.length();
         
         // Create the buffer to store the resulting string
 StringBuffer result = new StringBuffer (length);
         
         // Loop for the characters of the original string
 for(int i=0; i<length; i++) {
             // Grab the next character and determine how to handle it
 char c = text.charAt(i);
             switch (c) {
                 case '<': {
                     // We have a less than, grab the maximum tag length of characters
 // or the remaining characters which follow and determine if it
 // is the start of a tag to ignore.
 String tmp = ICheatSheetResource.EMPTY_STRING;
                     if(i+MAXIMUM_TAG_LENGTH < length)
                         tmp = text.substring(i, i+MAXIMUM_TAG_LENGTH).toLowerCase();
                     else {
                         tmp = text.substring(i, length).toLowerCase();
                     }
                     if(tmp.startsWith(IParserTags.BOLD_START_TAG) || tmp.startsWith(IParserTags.BOLD_END_TAG) || tmp.startsWith(IParserTags.BREAK_TAG)) {
                         // We have a tag to ignore so just emit the character
 result.append(c);
                     } else {
                         // We have detemined that it is just a less than
 // so emit the XML escaped counterpart
 result.append(IParserTags.LESS_THAN);
                     }
                     break; }
                 case '>': {
                     // We have a greater than, grab the maximum tag length of characters
 // or the starting characters which come before and determine if it
 // is the end of a tag to ignore.
 String tmp = ICheatSheetResource.EMPTY_STRING;
                     if(i>=MAXIMUM_TAG_LENGTH) {
                         tmp = text.substring(i-MAXIMUM_TAG_LENGTH, i+1).toLowerCase();
                     } else {
                         tmp = text.substring(0, i+1).toLowerCase();
                     }
                     if(tmp.endsWith(IParserTags.BOLD_START_TAG) || tmp.endsWith(IParserTags.BOLD_END_TAG) || tmp.endsWith(IParserTags.BREAK_TAG)) {
                         // We have a tag to ignore so just emit the character
 result.append(c);
                     } else {
                         // We have detemined that it is just a greater than
 // so emit the XML escaped counterpart
 result.append(IParserTags.GREATER_THAN);
                     }
                     break; }
                 case '&':
                     // We have an ampersand so emit the XML escaped counterpart
 result.append(IParserTags.AMPERSAND);
                     break;
                 case '\'':
                     // We have an apostrophe so emit the XML escaped counterpart
 result.append(IParserTags.APOSTROPHE);
                     break;
                 case '"':
                     // We have a quote so emit the XML escaped counterpart
 result.append(IParserTags.QUOTE);
                     break;
                 case '\t':
                     // We have a tab, replace with a space
 result.append(' ');
                     break;
                 default:
                     // We have a character that does not require escaping
 result.append(c);
                     break;
             }
         }
         return result;
     }

     private Node findNode(Node startNode, String nodeName) {
         if(startNode == null) {
             return null;
         }

         if(startNode.getNodeName().equals(nodeName)) {
             return startNode;
         }

         NodeList nodes = startNode.getChildNodes();
         for (int i = 0; i < nodes.getLength(); i++) {
             Node node = nodes.item(i);
             if(node.getNodeName().equals(nodeName)) {
                 return node;
             }
         }
         
         return null;
     }

     private void handleExecutable(IExecutableItem item, Node executableNode, AbstractExecutable executable) throws CheatSheetParserException {
         Assert.isNotNull(item);
         Assert.isNotNull(executableNode);

         String [] params = null;

         if (executable instanceof CheatSheetCommand) {
             commandCount++;
         }
         if (executable instanceof Action) {
             actionCount++;
         }

         NamedNodeMap attributes = executableNode.getAttributes();
         if (attributes != null) {
             for (int x = 0; x < attributes.getLength(); x++) {
                 Node attribute = attributes.item(x);
                 String attributeName = attribute.getNodeName();
                 if (attribute == null || attributeName == null)
                     continue;
                 if (attributeName.equals(IParserTags.CONFIRM)) {
                     executable.setConfirm(attribute.getNodeValue().equals(TRUE_STRING));}
                 else if (attributeName.equals(IParserTags.WHEN)) {
                     executable.setWhen(attribute.getNodeValue());
                 } else if (attributeName.equals(IParserTags.REQUIRED)) {
                     executable.setRequired(attribute.getNodeValue().equals(TRUE_STRING));
                 } else if (attributeName.equals(IParserTags.TRANSLATE)) {
                     // Translation hint, no semantic effect
 } else if (executable.hasParams() && attributeName.startsWith(IParserTags.PARAM)) {
                     try {
                         if(params == null) {
                             params = new String [9];
                         }
                         String paramNum = attributeName.substring(IParserTags.PARAM.length());
                         int num = Integer.parseInt(paramNum)-1;
                         
                         if(num>-1 && num<9){
                             params[num] = attribute.getNodeValue();
                         } else {
                             String message = NLS.bind(Messages.ERROR_PARSING_PARAM_INVALIDRANGE, (new Object [] {attributeName, paramNum}));
                             addStatus(IStatus.ERROR, message, null);
                         }
                     } catch(NumberFormatException e) {
                         String message = Messages.ERROR_PARSING_PARAM_INVALIDNUMBER;
                         addStatus(IStatus.ERROR, message, e);
                     }
                 } else if (!executable.handleAttribute(attribute)) {
                     String message = NLS.bind(Messages.WARNING_PARSING_UNKNOWN_ATTRIBUTE, (new Object [] {attributeName, executableNode.getNodeName()}));
                     addStatus(IStatus.WARNING, message, null);
                 }
             }
             String errorMessage = executable.checkAttributes(executableNode);
             if (errorMessage != null) {
                 throw new CheatSheetParserException(errorMessage);
             }
         }
         checkForNoChildren(executableNode);
         executable.setParams(params);
         item.setExecutable(executable);
     }

     private void checkForNoChildren(Node parentNode) {
         NodeList nodes = parentNode.getChildNodes();
         for (int i = 0; i < nodes.getLength(); i++) {
             Node node = nodes.item(i);
             if(node.getNodeType() != Node.TEXT_NODE && node.getNodeType() != Node.COMMENT_NODE ) {
                 String message = NLS.bind(Messages.WARNING_PARSING_UNKNOWN_ELEMENT, (new Object [] {node.getNodeName(), parentNode.getNodeName()}));
                 addStatus(IStatus.WARNING, message, null);
             }
         }
     }

     private void handleCheatSheetAttributes(CheatSheet cheatSheet, Node cheatSheetNode) throws CheatSheetParserException {
         Assert.isNotNull(cheatSheet);
         Assert.isNotNull(cheatSheetNode);
         Assert.isTrue(cheatSheetNode.getNodeName().equals(IParserTags.CHEATSHEET));

         boolean title = false;

         NamedNodeMap attributes = cheatSheetNode.getAttributes();
         if (attributes != null) {
             for (int x = 0; x < attributes.getLength(); x++) {
                 Node attribute = attributes.item(x);
                 String attributeName = attribute.getNodeName();
                 if (attribute == null || attributeName == null)
                     continue;

                 if (attributeName.equals(IParserTags.TITLE)) {
                     title = true;
                     cheatSheet.setTitle(attribute.getNodeValue());
                 } else {
                     String message = NLS.bind(Messages.WARNING_PARSING_UNKNOWN_ATTRIBUTE, (new Object [] {attributeName, cheatSheetNode.getNodeName()}));
                     addStatus(IStatus.WARNING, message, null);
                 }
             }
         }

         if(!title) {
             String message = NLS.bind(Messages.ERROR_PARSING_NO_TITLE, (new Object [] {cheatSheetNode.getNodeName()}));
             throw new CheatSheetParserException(message);
         }
     }

     private void handleConditionalSubItem(Item item, Node conditionalSubItemNode) throws CheatSheetParserException {
         Assert.isNotNull(item);
         Assert.isNotNull(conditionalSubItemNode);
         Assert.isTrue(conditionalSubItemNode.getNodeName().equals(IParserTags.CONDITIONALSUBITEM));

         ConditionalSubItem conditionalSubItem = new ConditionalSubItem();

         boolean condition = false;

         // Handle attributes
 NamedNodeMap attributes = conditionalSubItemNode.getAttributes();
         if (attributes != null) {
             for (int x = 0; x < attributes.getLength(); x++) {
                 Node attribute = attributes.item(x);
                 String attributeName = attribute.getNodeName();
                 if (attribute == null || attributeName == null)
                     continue;

                 if (attributeName.equals(IParserTags.CONDITION)) {
                     condition = true;
                     conditionalSubItem.setCondition(attribute.getNodeValue());
                 } else {
                     String message = NLS.bind(Messages.WARNING_PARSING_UNKNOWN_ATTRIBUTE, (new Object [] {attributeName, conditionalSubItemNode.getNodeName()}));
                     addStatus(IStatus.WARNING, message, null);
                 }
             }
         }

         if(!condition) {
             String message = NLS.bind(Messages.ERROR_PARSING_NO_CONDITION, (new Object [] {conditionalSubItemNode.getNodeName()}));
             throw new CheatSheetParserException(message);
         }

         boolean subitem = false;

         // Handle nodes
 NodeList nodes = conditionalSubItemNode.getChildNodes();
         for (int i = 0; i < nodes.getLength(); i++) {
             Node node = nodes.item(i);

             if(node.getNodeName().equals(IParserTags.SUBITEM)) {
                 subitem = true;
                 handleSubItem(conditionalSubItem, node);
             } else {
                 if(node.getNodeType() != Node.TEXT_NODE && node.getNodeType() != Node.COMMENT_NODE ) {
                     String message = NLS.bind(Messages.WARNING_PARSING_UNKNOWN_ELEMENT, (new Object [] {node.getNodeName(), conditionalSubItemNode.getNodeName()}));
                     addStatus(IStatus.WARNING, message, null);
                 }
             }
         }

         if(!subitem) {
             String message = NLS.bind(Messages.ERROR_PARSING_NO_SUBITEM, (new Object [] {conditionalSubItemNode.getNodeName()}));
             throw new CheatSheetParserException(message);
         }

         item.addSubItem(conditionalSubItem);
     }

     private void handleDescription(Item item, Node startNode) throws CheatSheetParserException {
         Assert.isNotNull(item);
         Assert.isNotNull(startNode);

         Node descriptionNode = findNode(startNode, IParserTags.DESCRIPTION);
         
         if(descriptionNode != null) {
             String text = handleMarkedUpText(descriptionNode, startNode, IParserTags.DESCRIPTION);
             item.setDescription(text);
         } else {
             Node parentNode = startNode;
             if( startNode.getNodeName().equals(IParserTags.DESCRIPTION) ) {
                 parentNode = startNode.getParentNode();
             }
             String message = NLS.bind(Messages.ERROR_PARSING_NO_DESCRIPTION, (new Object [] {parentNode.getNodeName()}));
             throw new CheatSheetParserException(message);
         }
     }

     private String handleMarkedUpText(Node nodeContainingText, Node startNode, String nodeName ) {
         NodeList nodes = nodeContainingText.getChildNodes();
         StringBuffer text = new StringBuffer ();
         
         boolean containsMarkup = false;
         
         // The documentation for the content file specifies
 // that leading whitespace should be ignored at the
 // beginning of a description or after a <br/>. This
 // applies also to <onCompletion> elements.
 // See Bug 129208 and Bug 131185
 boolean isLeadingTrimRequired = true;

         for (int i = 0; i < nodes.getLength(); i++) {
             Node node = nodes.item(i);
             if(node.getNodeType() == Node.TEXT_NODE) {
                 String nodeValue = node.getNodeValue();
                 if (isLeadingTrimRequired) {
                     nodeValue = trimLeadingWhitespace(nodeValue);
                 }
                 text.append(nodeValue);
                 isLeadingTrimRequired = false;
             } else if(node.getNodeType() == Node.ELEMENT_NODE) {
                 // handle <b></b> and <br/>
 if(node.getNodeName().equals(IParserTags.BOLD)) {
                     containsMarkup = true;
                     text.append(IParserTags.BOLD_START_TAG);
                     text.append(node.getFirstChild().getNodeValue());
                     text.append(IParserTags.BOLD_END_TAG);
                     isLeadingTrimRequired = false;
                 } else if(node.getNodeName().equals(IParserTags.BREAK)) {
                     containsMarkup = true;
                     text.append(IParserTags.BREAK_TAG);
                     isLeadingTrimRequired = true;
                 } else {
                     warnUnknownMarkupElement(startNode, nodeName, node);
                 }
             }
         }

         if(containsMarkup) {
             text = escapeXMLCharacters(text);
             text.insert(0, IParserTags.FORM_START_TAG);
             text.append(IParserTags.FORM_END_TAG);
         } else {
             deTab(text);
         }

         // Remove the new line, form feed and tab chars
 return text.toString().trim();
     }

     // Replace any tabs with spaces

     private void deTab(StringBuffer text) {
         for (int i = 0; i < text.length(); i++) {
             if (text.charAt(i) == '\t') {
                 text.setCharAt(i, ' ');
             }
         }
     }

     private String trimLeadingWhitespace(String nodeValue) {
         int firstNonWhitespaceIndex = 0;
         while (firstNonWhitespaceIndex < nodeValue.length() &&
                 Character.isWhitespace(nodeValue.charAt(firstNonWhitespaceIndex))) {
             firstNonWhitespaceIndex++;
         }
         if (firstNonWhitespaceIndex > 0) {
             return nodeValue.substring(firstNonWhitespaceIndex, nodeValue.length());
         }
         return nodeValue;
     }

     /*
      * Write a warning to the log
      */
     private void warnUnknownMarkupElement(Node startNode, String nodeName, Node node) {
         Node parentNode = startNode;
         if( startNode.getNodeName().equals(nodeName) ) {
             parentNode = startNode.getParentNode();
         }
         String message;
         if (IParserTags.DESCRIPTION.equals(nodeName)) {
             message = NLS.bind(Messages.WARNING_PARSING_DESCRIPTION_UNKNOWN_ELEMENT, (new Object [] {parentNode.getNodeName(), node.getNodeName()}));
         } else {
             message = NLS.bind(Messages.WARNING_PARSING_ON_COMPLETION_UNKNOWN_ELEMENT, (new Object [] {parentNode.getNodeName(), node.getNodeName()}));
         }
         addStatus(IStatus.WARNING, message, null);

     }
     
     private void handleOnCompletion(Item item, Node onCompletionNode) {
         String text = handleMarkedUpText(onCompletionNode, onCompletionNode, IParserTags.ON_COMPLETION);
         item.setCompletionMessage(text);
     }
     
     private void handleIntroNode(CheatSheet cheatSheet, Node introNode)
             throws CheatSheetParserException {
         Item introItem = new Item();
         introItem.setTitle(Messages.CHEAT_SHEET_INTRO_TITLE);

         handleIntroAttributes(introItem, introNode);
         
         boolean hasDescription = false;
         
         NodeList nodes = introNode.getChildNodes();
         for (int i = 0; i < nodes.getLength(); i++) {
             Node node = nodes.item(i);

             if(node.getNodeName().equals(IParserTags.DESCRIPTION)) {
                 if (hasDescription) {
                     String message = NLS.bind(Messages.ERROR_PARSING_MULTIPLE_DESCRIPTION, (new Object [] {introNode.getNodeName()}));
                     addStatus(IStatus.ERROR, message, null);
                 } else {
                     hasDescription = true;
                     handleDescription(introItem, node);
                 }
             } else {
                 if(node.getNodeType() != Node.TEXT_NODE && node.getNodeType() != Node.COMMENT_NODE ) {
                     String message = NLS.bind(Messages.WARNING_PARSING_UNKNOWN_ELEMENT, (new Object [] {node.getNodeName(), introNode.getNodeName()}));
                     addStatus(IStatus.WARNING, message, null);
                 }
             }
         }

         if(!hasDescription) {
             String message = NLS.bind(Messages.ERROR_PARSING_NO_DESCRIPTION, (new Object [] {introNode.getNodeName()}));
             addStatus(IStatus.ERROR, message, null);
         }

         cheatSheet.setIntroItem(introItem);
     }

     private void handleIntroAttributes(Item item, Node introNode) {
         Assert.isNotNull(item);
         Assert.isNotNull(introNode);

         NamedNodeMap attributes = introNode.getAttributes();
         if (attributes != null) {
             for (int x = 0; x < attributes.getLength(); x++) {
                 Node attribute = attributes.item(x);
                 String attributeName = attribute.getNodeName();
                 if (attribute == null || attributeName == null)
                     continue;

                 if (attributeName.equals(IParserTags.CONTEXTID)) {
                     item.setContextId(attribute.getNodeValue());
                 } else if (attributeName.equals(IParserTags.HREF)) {
                     item.setHref(attribute.getNodeValue());
                 } else {
                     String message = NLS.bind(Messages.WARNING_PARSING_UNKNOWN_ATTRIBUTE, (new Object [] {attributeName, introNode.getNodeName()}));
                     addStatus(IStatus.WARNING, message, null);
                 }
             }
         }
     }

     private Item handleItem(Node itemNode) throws CheatSheetParserException {
         Assert.isNotNull(itemNode);
         Assert.isTrue(itemNode.getNodeName().equals(IParserTags.ITEM));

         Item item = new Item();

         handleItemAttributes(item, itemNode);

         boolean hasDescription = false;
         
         NodeList nodes = itemNode.getChildNodes();
         
         IncompatibleSiblingChecker checker = new IncompatibleSiblingChecker(this, itemNode);
         for (int i = 0; i < nodes.getLength(); i++) {
             Node node = nodes.item(i);
             checker.checkElement(node.getNodeName());
             if(node.getNodeName().equals(IParserTags.ACTION)) {
                 handleExecutable(item, node, new Action());
             } else if(node.getNodeName().equals(IParserTags.COMMAND)) {
                 handleExecutable(item, node, new CheatSheetCommand());
             } else if(node.getNodeName().equals(IParserTags.DESCRIPTION)) {
                 if (hasDescription) {
                     String message = NLS.bind(Messages.ERROR_PARSING_MULTIPLE_DESCRIPTION, (new Object [] {itemNode.getNodeName()}));
                     addStatus(IStatus.ERROR, message, null);
                 } else {
                     hasDescription = true;
                     handleDescription(item, node);
                 }
             } else if(node.getNodeName().equals(IParserTags.ON_COMPLETION)) {
                 handleOnCompletion(item, node);
             } else if(node.getNodeName().equals(IParserTags.SUBITEM)) {
                 handleSubItem(item, node);
             } else if(node.getNodeName().equals(IParserTags.CONDITIONALSUBITEM)) {
                 handleConditionalSubItem(item, node);
             } else if(node.getNodeName().equals(IParserTags.REPEATEDSUBITM)) {
                 handleRepeatedSubItem(item, node);
             } else if(node.getNodeName().equals(IParserTags.PERFORMWHEN)) {
                 handlePerformWhen(item, node);
             } else {
                 if(node.getNodeType() != Node.TEXT_NODE && node.getNodeType() != Node.COMMENT_NODE ) {
                     String message = NLS.bind(Messages.WARNING_PARSING_UNKNOWN_ELEMENT, (new Object [] {node.getNodeName(), itemNode.getNodeName()}));
                     addStatus(IStatus.WARNING, message, null);
                 }
             }
         }

         if(!hasDescription) {
             String message = NLS.bind(Messages.ERROR_PARSING_NO_DESCRIPTION, (new Object [] {itemNode.getNodeName()}));
             addStatus(IStatus.ERROR, message, null);
         }
         
         return item;
     }

     private void handleItemAttributes(Item item, Node itemNode) throws CheatSheetParserException {
         Assert.isNotNull(item);
         Assert.isNotNull(itemNode);

         ArrayList itemExtensionElements = new ArrayList ();

         boolean title = false;

         NamedNodeMap attributes = itemNode.getAttributes();
         if (attributes != null) {
             for (int x = 0; x < attributes.getLength(); x++) {
                 Node attribute = attributes.item(x);
                 String attributeName = attribute.getNodeName();
                 if (attribute == null || attributeName == null)
                     continue;

                 if (attributeName.equals(IParserTags.TITLE)) {
                     title = true;
                     item.setTitle(attribute.getNodeValue());
                 } else if (attributeName.equals(IParserTags.CONTEXTID)) {
                     item.setContextId(attribute.getNodeValue());
                 } else if (attributeName.equals(IParserTags.HREF)) {
                     item.setHref(attribute.getNodeValue());
                 } else if (attributeName.equals(IParserTags.SKIP)) {
                     item.setSkip(attribute.getNodeValue().equals(TRUE_STRING));
                 } else if (attributeName.equals(IParserTags.DIALOG)) {
                     item.setDialog(attribute.getNodeValue().equals(TRUE_STRING));
                 } else {
                     AbstractItemExtensionElement[] ie = handleUnknownItemAttribute(attribute, itemNode);
                     if (ie != null) {
                         itemExtensionElements.add(ie);
                     } else {
                         String message = NLS.bind(Messages.WARNING_PARSING_UNKNOWN_ATTRIBUTE, (new Object [] {attributeName, itemNode.getNodeName()}));
                         addStatus(IStatus.WARNING, message, null);
                     }
                 }
             }
         }

         if(!title) {
             String message = NLS.bind(Messages.ERROR_PARSING_NO_TITLE, (new Object [] {itemNode.getNodeName()}));
             throw new CheatSheetParserException(message);
         }

         if (itemExtensionElements != null)
             item.setItemExtensions(itemExtensionElements);
     }

     private void handlePerformWhen(IPerformWhenItem item, Node performWhenNode) throws CheatSheetParserException {
         Assert.isNotNull(item);
         Assert.isNotNull(performWhenNode);
         Assert.isTrue(performWhenNode.getNodeName().equals(IParserTags.PERFORMWHEN));

         PerformWhen performWhen = new PerformWhen();

          boolean condition = false;

         // Handle attributes
 NamedNodeMap attributes = performWhenNode.getAttributes();
         if (attributes != null) {
             for (int x = 0; x < attributes.getLength(); x++) {
                 Node attribute = attributes.item(x);
                 String attributeName = attribute.getNodeName();
                 if (attribute == null || attributeName == null)
                     continue;

                 if (attributeName.equals(IParserTags.CONDITION)) {
                     condition = true;
                     performWhen.setCondition(attribute.getNodeValue());
                 } else {
                     String message = NLS.bind(Messages.WARNING_PARSING_UNKNOWN_ATTRIBUTE, (new Object [] {attributeName, performWhenNode.getNodeName()}));
                     addStatus(IStatus.WARNING,message, null);
                 }
             }
         }

         if(!condition) {
             String message = NLS.bind(Messages.ERROR_PARSING_NO_CONDITION, (new Object [] {performWhenNode.getNodeName()}));
             throw new CheatSheetParserException(message);
         }

         boolean exeutable = false;

         // Handle nodes
 NodeList nodes = performWhenNode.getChildNodes();
         for (int i = 0; i < nodes.getLength(); i++) {
             Node node = nodes.item(i);
             if(node.getNodeName().equals(IParserTags.ACTION)) {
                 exeutable = true;
                 handleExecutable(performWhen, node, new Action());
             } else if(node.getNodeName().equals(IParserTags.COMMAND)) {
                 exeutable = true;
                 handleExecutable(performWhen, node, new CheatSheetCommand());
             } else {
                 if(node.getNodeType() != Node.TEXT_NODE && node.getNodeType() != Node.COMMENT_NODE ) {
                     String message = NLS.bind(Messages.WARNING_PARSING_UNKNOWN_ELEMENT, (new Object [] {node.getNodeName(), performWhenNode .getNodeName()}));
                     addStatus(IStatus.WARNING, message, null);
                 }
             }
         }

         if(!exeutable) {
             String message = NLS.bind(Messages.ERROR_PARSING_NO_ACTION, (new Object [] {performWhenNode.getNodeName()}));
             throw new CheatSheetParserException(message);
         }

         item.setPerformWhen(performWhen);
     }

     private void handleRepeatedSubItem(Item item, Node repeatedSubItemNode) throws CheatSheetParserException {
         Assert.isNotNull(item);
         Assert.isNotNull(repeatedSubItemNode);
         Assert.isTrue(repeatedSubItemNode.getNodeName().equals(IParserTags.REPEATEDSUBITM));

         RepeatedSubItem repeatedSubItem = new RepeatedSubItem();

         boolean values = false;

         // Handle attributes
 NamedNodeMap attributes = repeatedSubItemNode.getAttributes();
         if (attributes != null) {
             for (int x = 0; x < attributes.getLength(); x++) {
                 Node attribute = attributes.item(x);
                 String attributeName = attribute.getNodeName();
                 if (attribute == null || attributeName == null)
                     continue;

                 if (attributeName.equals(IParserTags.VALUES)) {
                     values = true;
                     repeatedSubItem.setValues(attribute.getNodeValue());
                 } else {
                     String message = NLS.bind(Messages.WARNING_PARSING_UNKNOWN_ATTRIBUTE, (new Object [] {attributeName, repeatedSubItemNode.getNodeName()}));
                     addStatus(IStatus.WARNING, message, null);
                 }
             }
         }

         if(!values) {
             String message = NLS.bind(Messages.ERROR_PARSING_NO_VALUES, (new Object [] {repeatedSubItemNode.getNodeName()}));
             throw new CheatSheetParserException(message);
         }

         boolean subitem = false;

         // Handle nodes
 NodeList nodes = repeatedSubItemNode.getChildNodes();
         for (int i = 0; i < nodes.getLength(); i++) {
             Node node = nodes.item(i);

             if(node.getNodeName().equals(IParserTags.SUBITEM)) {
                 subitem = true;
                 handleSubItem(repeatedSubItem, node);
             } else {
                 if(node.getNodeType() != Node.TEXT_NODE && node.getNodeType() != Node.COMMENT_NODE ) {
                     String message = NLS.bind(Messages.WARNING_PARSING_UNKNOWN_ELEMENT, (new Object [] {node.getNodeName(), repeatedSubItemNode.getNodeName()}));
                     addStatus(IStatus.WARNING, message, null);
                 }
             }
         }

         if(!subitem) {
             String message = NLS.bind(Messages.ERROR_PARSING_NO_SUBITEM, (new Object [] {repeatedSubItemNode.getNodeName()}));
             throw new CheatSheetParserException(message);
         }

         item.addSubItem(repeatedSubItem);
     }

     private void handleSubItem(ISubItemItem item, Node subItemNode) throws CheatSheetParserException {
         Assert.isNotNull(item);
         Assert.isNotNull(subItemNode);
         Assert.isTrue(subItemNode.getNodeName().equals(IParserTags.SUBITEM));

         SubItem subItem = new SubItem();

         handleSubItemAttributes(subItem, subItemNode);
         
         IncompatibleSiblingChecker checker = new IncompatibleSiblingChecker(this, subItemNode);

         NodeList nodes = subItemNode.getChildNodes();
         for (int i = 0; i < nodes.getLength(); i++) {
             Node node = nodes.item(i);
             checker.checkElement(node.getNodeName());

             if(node.getNodeName().equals(IParserTags.ACTION)) {
                 handleExecutable(subItem, node, new Action());
             } else if(node.getNodeName().equals(IParserTags.COMMAND)) {
                 handleExecutable(subItem, node, new CheatSheetCommand());
             } else if(node.getNodeName().equals(IParserTags.PERFORMWHEN)) {
                 handlePerformWhen(subItem, node);
             } else {
                 if(node.getNodeType() != Node.TEXT_NODE && node.getNodeType() != Node.COMMENT_NODE ) {
                     String message = NLS.bind(Messages.WARNING_PARSING_UNKNOWN_ELEMENT, (new Object [] {node.getNodeName(), subItemNode.getNodeName()}));
                     addStatus(IStatus.WARNING, message, null);
                 }
             }
         }
         item.addSubItem(subItem);
     }

     private void handleSubItemAttributes(SubItem subItem, Node subItemNode) throws CheatSheetParserException {
         Assert.isNotNull(subItem);
         Assert.isNotNull(subItemNode);

         boolean label = false;

         NamedNodeMap attributes = subItemNode.getAttributes();
         if (attributes != null) {
             for (int x = 0; x < attributes.getLength(); x++) {
                 Node attribute = attributes.item(x);
                 String attributeName = attribute.getNodeName();
                 if (attribute == null || attributeName == null)
                     continue;

                 if (attributeName.equals(IParserTags.LABEL)) {
                     label = true;
                     subItem.setLabel(attribute.getNodeValue());
                 } else if (attributeName.equals(IParserTags.SKIP)) {
                     subItem.setSkip(attribute.getNodeValue().equals(TRUE_STRING));
                 } else if (attributeName.equals(IParserTags.WHEN)) {
                     subItem.setWhen(attribute.getNodeValue());
                 } else {
                     String message = NLS.bind(Messages.WARNING_PARSING_UNKNOWN_ATTRIBUTE, (new Object [] {attributeName, subItemNode.getNodeName()}));
                     addStatus(IStatus.WARNING, message, null);
                 }
             }
         }

         if(!label) {
             String message = NLS.bind(Messages.ERROR_PARSING_NO_LABEL, (new Object [] {subItemNode.getNodeName()}));
             throw new CheatSheetParserException(message);
         }
     }

     private AbstractItemExtensionElement[] handleUnknownItemAttribute(Node item, Node node) {
         ArrayList al = new ArrayList ();
         if (itemExtensionContainerList == null)
             return null;

         for (int i = 0; i < itemExtensionContainerList.size(); i++) {
             CheatSheetItemExtensionElement itemExtensionElement = (CheatSheetItemExtensionElement) itemExtensionContainerList.get(i);

             if (itemExtensionElement.getItemAttribute().equals(item.getNodeName())) {
                 AbstractItemExtensionElement itemElement = itemExtensionElement.createInstance();
                 if(itemElement != null) {
                     itemElement.handleAttribute(item.getNodeValue());
                     al.add(itemElement);
                 }
             }
         }

         if(al.size() == 0) {
             String message = NLS.bind(Messages.WARNING_PARSING_UNKNOWN_ATTRIBUTE, (new Object [] {item.getNodeName(), node.getNodeName()}));
             addStatus(IStatus.WARNING, message, null);
         }
         return (AbstractItemExtensionElement[])al.toArray(new AbstractItemExtensionElement[al.size()]);
     }

     public ICheatSheet parse(URL url, String pluginId, int cheatSheetKind) {
         return parse(new ParserInput(url, pluginId), cheatSheetKind);
     }
     
     public ICheatSheet parse(ParserInput input, int cheatSheetKind) {
         status = Status.OK_STATUS;
         commandCount = 0;
         actionCount = 0;
         if(input == null) {
             return null;
         }

         InputStream is = null;
         InputSource inputSource = null;
         String filename = ""; //$NON-NLS-1$
 URL url = input.getUrl();

         if (input.getXml() != null) {
             StringReader reader = new StringReader (input.getXml());
             inputSource = new InputSource (reader);
         } else if (input.getUrl() != null){
             try {
                 is = url.openStream();
     
                 if (is != null) {
                     inputSource = new InputSource (is);
                 }
             } catch (Exception e) {
                 String message = NLS.bind(Messages.ERROR_OPENING_FILE, (new Object [] {url.getFile()}));
                 addStatus(IStatus.ERROR, message, e);
                 return null;
             }
         } else {
             return null;
         }
         
         if (input.getUrl() != null){
             filename = url.getFile();
         }

         Document document;
         try {
             if(documentBuilder == null) {
                 addStatus(IStatus.ERROR, Messages.ERROR_DOCUMENT_BUILDER_NOT_INIT, null);
                 return null;
             }
             document = documentBuilder.parse(inputSource);
         } catch (IOException e) {
             String message = NLS.bind(Messages.ERROR_OPENING_FILE_IN_PARSER, (new Object [] {filename}));
             addStatus(IStatus.ERROR, message, e);
             return null;
         } catch (SAXParseException spe) {
             String message = NLS.bind(Messages.ERROR_SAX_PARSING_WITH_LOCATION, (new Object [] {filename, new Integer (spe.getLineNumber()), new Integer (spe.getColumnNumber())}));
             addStatus(IStatus.ERROR, message, spe);
             return null;
         } catch (SAXException se) {
             String message = NLS.bind(Messages.ERROR_SAX_PARSING, (new Object [] {filename}));
             addStatus(IStatus.ERROR, message, se);
             return null;
         } finally {
             try {
                 is.close();
             } catch (Exception e) {
             }
         }

         // process dynamic content, normalize paths
 if (processor == null) {
             DocumentReader reader = new DocumentReader();
             processor = new DocumentProcessor(new ProcessorHandler[] {
                 new FilterHandler(CheatSheetEvaluationContext.getContext()),
                 new NormalizeHandler(),
                 new IncludeHandler(reader, Platform.getNL()),
                 new ExtensionHandler(reader, Platform.getNL())
             });
         }
         String documentPath = null;
         if (input.getPluginId() != null) {
             documentPath = '/' + input.getPluginId() + input.getUrl().getPath();
         }
         processor.process(UAElementFactory.newElement(document.getDocumentElement()), documentPath);
         
         if ( cheatSheetKind == COMPOSITE_ONLY || (cheatSheetKind == ANY && isComposite(document))) {
             CompositeCheatSheetParser compositeParser = new CompositeCheatSheetParser();
             CompositeCheatSheetModel result = compositeParser.parseCompositeCheatSheet(document, input.getUrl());
             status = compositeParser.getStatus();
             return result;
         }
         try {
             return parseCheatSheet(document);
         } catch(CheatSheetParserException e) {
             addStatus(IStatus.ERROR, e.getMessage(), e);
         }
         return null;
     }

     private boolean isComposite(Document document) {
         if (document != null) {
             Node rootnode = document.getDocumentElement();
             // Is the root node compositeCheatsheet?
 return rootnode.getNodeName().equals(ICompositeCheatsheetTags.COMPOSITE_CHEATSHEET) ;
         }
         return false;
     }

     private CheatSheet parseCheatSheet(Document document) throws CheatSheetParserException {
         // If the document passed is null return a null tree and update the status
 if (document != null) {
             Node rootnode = document.getDocumentElement();
             
             // Is the root node really <cheatsheet>?
 if( !rootnode.getNodeName().equals(IParserTags.CHEATSHEET) ) {
                 throw new CheatSheetParserException(Messages.ERROR_PARSING_CHEATSHEET_ELEMENT);
             }

             // Create the cheat sheet model object
 CheatSheet cheatSheet = new CheatSheet();

             handleCheatSheetAttributes(cheatSheet, rootnode);

             boolean hasItem = false;
             boolean hasIntro = false;
             
             CheatSheetRegistryReader reader = CheatSheetRegistryReader.getInstance();
             itemExtensionContainerList = reader.readItemExtensions();
             
             NodeList nodes = rootnode.getChildNodes();
             for (int i = 0; i < nodes.getLength(); i++) {
                 Node node = nodes.item(i);

                 if(node.getNodeName().equals(IParserTags.ITEM)) {
                     hasItem = true;
                    Item item = handleItem(node);
                    cheatSheet.addItem(item);
                } else if(node.getNodeName().equals(IParserTags.INTRO)) {
                    if (hasIntro) {
                        addStatus(IStatus.ERROR, Messages.ERROR_PARSING_MORE_THAN_ONE_INTRO, null);
                    } else {
                        hasIntro = true;
                        handleIntroNode(cheatSheet, node);
                    }
                } else {
                    if(node.getNodeType() != Node.TEXT_NODE && node.getNodeType() != Node.COMMENT_NODE ) {
                        String message = NLS.bind(Messages.WARNING_PARSING_UNKNOWN_ELEMENT, (new Object [] {node.getNodeName(), rootnode.getNodeName()}));
                        addStatus(IStatus.WARNING, message, null);
                    }
                }
            }
            
            if(!hasIntro) {
                addStatus(IStatus.ERROR, Messages.ERROR_PARSING_NO_INTRO, null);
            }
            if(!hasItem) {
                addStatus(IStatus.ERROR, Messages.ERROR_PARSING_NO_ITEM, null);
            }

            //handleIntro(cheatSheet, document);

            //handleItems(cheatSheet, document);

            if (status.getSeverity() == IStatus.ERROR) {
                return null;
            }
            
            cheatSheet.setContainsCommandOrAction(actionCount != 0 || commandCount != 0);
            return cheatSheet;
        }
        throw new CheatSheetParserException(Messages.ERROR_PARSING_CHEATSHEET_CONTENTS);
    }

    /*
     * Normalizes composite cheat sheet-relative paths to simple cheat sheets into fully
     * qualified paths, e.g. for the path "tasks/mySimpleCheatSheet.xml" in composite cheat
     * sheet "/my.plugin/cheatsheets/myCompositeCheatSheet.xml", this normalizes to
     * "/my.plugin/cheatsheets/tasks/mySimpleCheatSheet.xml".
     *
     * This is necessary because with dynamic content we are pulling in tasks from other
     * plug-ins and those tasks have relative paths. It also only applies for cheat sheets
     * located in running plug-ins.
     */
    private class NormalizeHandler extends ProcessorHandler {
        
        private static final String ELEMENT_PARAM = "param"; //$NON-NLS-1$
 private static final String ATTRIBUTE_NAME = "name"; //$NON-NLS-1$
 private static final String ATTRIBUTE_VALUE = "value"; //$NON-NLS-1$
 private static final String NAME_PATH = "path"; //$NON-NLS-1$

        public short handle(UAElement element, String id) {
            if (id != null && ELEMENT_PARAM.equals(element.getElementName())) {
                String name = element.getAttribute(ATTRIBUTE_NAME);
                if (NAME_PATH.equals(name)) {
                    String value = element.getAttribute(ATTRIBUTE_VALUE);
                    if (value != null) {
                        int index = id.lastIndexOf('/');
                        element.setAttribute(ATTRIBUTE_VALUE, id.substring(0, index + 1) + value);
                    }
                }
                return HANDLED_CONTINUE;
            }
            return UNHANDLED;
        }
    }
}

